﻿#!/usr/bin/python -u
#_*_coding:utf-8_*_
'''
程序: ip4地址列表ping
程序：ip4pingHost.py
日期:2014-01-21 15:36
当前版本： 1.2.0
程序开发过程跟踪详细readme.md文件

'''
__version__ = "1.2.3"
__datetime__ = "20140127"
__copyright__ = "gyh9711@163.com"
__author__ = "gyh9711"

import sys
if sys.version.split(' ')[0] < '2.6':sys.exit(1)
import os
if os.name not in ['nt','posix']:exit(1)
import time
# import shutil,string
import platform
import re
import math
import array
import select
import binascii
import string
# from threading import Thread
import threading
from decimal import *
# from pprint import pprint
import cmd
# ip地址转换
import socket
import struct
#编码
# import chardet
#print chardet.detect(rawdata)
def str2uint(str):  
    # 得到始终是正数  
    return socket.ntohl(struct.unpack("I",socket.inet_aton(str))[0])
def str2int(str):  
    uint = socket.ntohl(struct.unpack("I",socket.inet_aton(str))[0])  
    # 先得到负数，再转换一下
    return struct.unpack("i", struct.pack('I', uint))[0]
def num2str(ip):
    if ip < 0:  
        ip = struct.unpack("I", struct.pack('i', ip))[0]  
    return socket.inet_ntoa(struct.pack('I',socket.htonl(ip)))

BASE_PATH = os.path.dirname(__file__)
sys.path.append(BASE_PATH)
## 这段代码是用于解决中文报错的问题  
reload(sys)  
sys.setdefaultencoding("utf8")
#辅助函数
def puts(mess,_help=None):
	'''puts()日志输出函数'''
	sys.stdout.write("%s\n"%mess)
	if _help != None:
		help(_help)

def getDateTime():
	'''返回当前时间'''
	return time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())

def getUptime():
	'''获取系统基本负载情况'''
	pass
	
#全局配置项
G_langD = {
	'zh':{
			'Usage':'''使用说明:
	python ip4pingHosts.py [hosts|filename|net/mask|shell] numberping[1~200|defalut 100]
	实例:
		ping net  by 50 pings
			python ip4pingHosts.py 10.245.101.0/24 50
		ping system /etc/hosts file
			python ip4pingHosts.py hosts
		ping my config hostsfile 
			python ip4pingHost.py c:\myhosts.txt
		shell
			pythono ip4pingHost.py shell
			
			''',
		},   #中文
	'en':{
			'Usage':'''Usage:
	python ip4pingHosts.py [hosts|filename|net/mask|shell] numberping[1~200|defalut 100]
	eaxmples:
		ping net  by 50 pings
			python ip4pingHosts.py 10.245.101.0/24 50
		ping system /etc/hosts file
			python ip4pingHosts.py hosts
		ping my config hostsfile 
			python ip4pingHost.py c:\myhosts.txt
		shell
			pythono ip4pingHost.py shell
		''',
		}    #英文
}  #输出信息对应语言配置
#程序配置
G_etcD = {
	'LOG':{'DEBUG':True,'LOG':True},
	'SHELL':{},   #运行 shell配置
	'PING':{
		'MAX_THREADS':100,  #并行ping 默认线程数
	},
	'PINGHOSTS':[], # 存放ping 的地址列表
	'LANG':G_langD['en'],  #配置输出语言 主要是根据输出编码，
	'SCANPORT':{  #端口扫描配置
		'MAX_THREADS':500,  #测试==1000,在win7 64中会报线程错误
		# 'DEFAULTSCANPORTS':[i for i in range(1,1024,1)],
	},
}

G_logD = {
	'DEBUG':[], #测试用日志
	'REPORT':[], #扫描结果日志
	'CURREPORT':{'PINGLIST':[],'COUNT':0,'UP':0,'DOWN':0,'STARTTIME':getDateTime(),'ENDTIME':None},  #最近一次扫描结果信息
}

#自动判断终端支持编码格式，显示英文或中文
if sys.getdefaultencoding() == 'utf8':
	G_etcD['LANG'] = G_langD['zh']
	
#网段计算
def ipNetCount(netStr=None):
	'''支持20~32位掩码计算，地址网太大，运行需要时间'''
	if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}[\\/][23]\d$',netStr):
		_netL=re.split(r'[\\/]',netStr)
		if str2int(_netL[0]) > 0:
			base = str2int(_netL[0])
		else:
			base = str2uint(_netL[0])
		base = int(base) - int(math.fmod(base,math.pow(2,int(32 - int(_netL[1])))))
		nums = int(math.pow(2,int(32 - int(_netL[1])))) - 1
		#检查掩码位是否符合要求
		if nums < pow(2,12):
			return[num2str(int(base)+i) for i in range(nums)[1:]]
		else:
			return False
	else:
		return False

'''
#==============================================================
#ping处理函数
#
#
#==============================================================
'''
class PYPING:
	'''本类主要核心代码来源于网络的两个ping代码，并修复其中的小bug
	 日期:2014-01-25 16:08
	'''
	ICMP_DATA_STR = 56  
	ICMP_TYPE = 8
	ICMP_TYPE_IP6 = 128
	ICMP_CODE = 0
	ICMP_CHECKSUM = 0
	ICMP_ID = None  #0
	ICMP_SEQ_NR = 0
	TIMEOUT = 1
	PINGSOCKETID = None
	IPTYPE = 0   # 0 表示ip4ip  1 表示ip6
	HOST = '127.0.0.1'
	def __init__(self,host='127.0.0.1',icmpId=None,timeout=None,ipType=0,):
		if timeout != None and isinstance(timeout,int):
			self.TIMEOUT = timeout
		if icmpId != None:
			self.ICMP_ID = icmpId
			#self.ICMP_SEQ_NR = icmpId
		if ipType==1 : # ip
			self.IPTYPE = 1
		self.HOST = host
	def ping(self):
		#开始执行Pin
		return self._pingNode(self.HOST)
	#  解决linux 与 windows系统上时间计算值问题
	def _getTime(self):
		if os.name == 'nt':
			return time.clock()
		elif os.name == 'posix':
			return time.time()
		else:
			return time.time()
	def _checkSum(self,packet):
		if len(packet) & 1:
			packet = packet + '\0'
		words = array.array('h', packet) 
		sum = 0
		for word in words:
			sum += (word & 0xffff) 
		hi = sum >> 16 
		lo = sum & 0xffff 
		sum = hi + lo
		sum = sum + (sum >> 16)
		return (~sum) & 0xffff
	def _construct(self):
		"""Constructs a ICMP echo packet of variable size
		"""
		size = self.ICMP_DATA_STR
		# size must be big enough to contain time sent
		if size < int(struct.calcsize("d")):
			return {'status':'down','time':"9999 ms",'ttl':256}
		# construct header
		if self.IPTYPE:#ipv6
			header = struct.pack('BbHHh', self.ICMP_TYPE_IP6,self.ICMP_CODE,self.ICMP_CHECKSUM,self.ICMP_ID,self.ICMP_SEQ_NR)
		else: #ipv4
			header = struct.pack('bbHHh', self.ICMP_TYPE,self.ICMP_CODE,self.ICMP_CHECKSUM,self.ICMP_ID, self.ICMP_SEQ_NR)
		# space for time
		size -= struct.calcsize("d")
		# construct payload based on size, may be omitted :)
		rest = ""
		# pad the rest of payload
		# rest += size * "X"
		rest += str(self.ICMP_ID)
		rest += (size-len(str(self.ICMP_ID))) * "X"
		# pack
		data = struct.pack("d", self._getTime()) + rest
		packet = header + data  # ping packet without checksum
		# construct header with correct checksum
		if self.IPTYPE:
			header = struct.pack('BbHHh', self.ICMP_TYPE,self.ICMP_CODE, self._checkSum(packet), self.ICMP_ID,self.ICMP_SEQ_NR)
		else:
			header = struct.pack('bbHHh', self.ICMP_TYPE,self.ICMP_CODE, self._checkSum(packet), self.ICMP_ID,self.ICMP_SEQ_NR)
		# ping packet *with* checksum
		packet = header + data 
		# a perfectly formatted ICMP echo packet
		return packet
	# 执行ping操作
	def _pingNode(self,node=None):
		"""Pings a node based on input given to the function.
		 资源参考来源于:  http://techpool.iteye.com/blog/661864
		return {'status':'up|down','time':d.ddd ms,'ttl':ddd}
		examples:
		{'status': 'down', 'time': "9999 ms",'ttl':126}
		{'status': 'up', 'time': '4.000 ms','ttl':230}
		"""
		size=self.ICMP_DATA_STR
		# if not a valid host, exit
		if self.IPTYPE:
			if socket.has_ipv6:
				try:
					info, port = socket.getaddrinfo(node, None)
					host = info[4][0]
					# do not print ipv6 twice if ipv6 address given as node
					if host == node: 
						noPrintIPv6adr = 1
				except:
					# _error("cannot resolve %s: Unknow host" % node)
					return {'status':'down','time':"9999 ms",'ttl':256}
			else:
				# _error("No support for IPv6 on this plattform")
				return {'status':'down','time':"9999 ms",'ttl':256}
		else:	# IPv4
			try:
				host = socket.gethostbyname(node)
			except:
				return {'status':'down','time':"9999 ms",'ttl':256}
		# trying to ping a network?
		if not self.IPTYPE:
			if int(string.split(host, ".")[-1]) == 0:
				return {'status':'down','time':"9999 ms",'ttl':256}
		# do some sanity check
		# Send the ping(s)
		start = 1; mint = 999; maxt = 0.0; avg = 0.0
		lost = 0; tsum = 0.000; tsumsq = 0.0
		lost += 1 # in case user hit ctrl-c
		# create the IPv6/IPv4 socket
		if self.IPTYPE:
			# can not create a raw socket if not root or setuid to root
			try:
				self.PINGSOCKETID = socket.socket(socket.AF_INET6, socket.SOCK_RAW,socket.getprotobyname("ipv6-icmp"))
			except socket.error, e:
				return {'status':'down','time':"9999 ms",'ttl':256}
		# IPv4
		else:
			# can not create a raw socket if not root or setuid to root
			try:
				self.PINGSOCKETID = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
			except socket.error, e:
				return {'status':'down','time':"9999 ms",'ttl':256}
		# pingId = os.getpid() & 0xFFFF
		packet = self._construct() # make a ping packet
		# send the ping
		try:
			self.PINGSOCKETID.sendto(packet,(node,1))
			while 1:
				# input, output, exceptional conditions
				iwtd, owtd, ewtd = select.select([self.PINGSOCKETID], [], [], self.TIMEOUT)
				if iwtd:  # ok, data on socket
					endtime = self._getTime()  # time packet received
					# read data (we only need the header)
					pong, address = self.PINGSOCKETID.recvfrom(size+48)
					lost -= 1 # in case user hit ctrl-c
					if self.IPTYPE:
						pongHeader = pong[0:8]
						pongType, pongCode, pongChksum, pongID, pongSeqnr = struct.unpack("bbHHh", pongHeader)
						# fetch starttime from pong
						starttime = struct.unpack("d", pong[8:16])[0]
						
					# IPv4
					else:
						# time to live
						rawPongHop = struct.unpack("s", pong[8])[0]
						# convert TTL from 8 bit to 16 bit integer
						pongHop = int(binascii.hexlify(str(rawPongHop)), 16)
						# fetch pong header
						pongHeader = pong[20:28]
						pongType, pongCode, pongChksum, pongID, pongSeqnr = struct.unpack("bbHHh", pongHeader)
						# fetch starttime from pong
						starttime = struct.unpack("d", pong[28:36])[0]
						_dataIcmpId = pong[36:36+len(str(self.ICMP_ID))]
					# NO data on socket - timeout waiting for answer
					triptime  = endtime - starttime # compute RRT
					tsum	 += triptime			# triptime for all packets (stddev)
					tsumsq   += triptime * triptime # triptime^2  for all packets (stddev)
					# compute statistic
					maxt = max ((triptime, maxt))
					mint = min ((triptime, mint))

					if pongID == self.ICMP_ID or self.ICMP_ID == int(_dataIcmpId):  #判断接收的包，可以会收到其它发出的ping包
						self.PINGSOCKETID.close()
						return {'status':'up','time':"%.3f ms"% (triptime*1000),'ttl':pongHop}
					else:
						pass
				else:
					return {'status':'down','time':"9999 ms",'ttl':256}
		except socket.error, e:
			return {'status':'down','time':"9999 ms",'ttl':256}
		# reset values
		pong = ""; iwtd = []
		# wait until there is data in the socket
		# print("while",self.PINGSOCKETID)
		self.PINGSOCKETID.close()

#多线程ping类
class PINGTHREAD(threading.Thread):
	_out = None
	def __init__(self,ip='127.0.0.1',icmpId=0,timeout= 0.1):
		threading.Thread.__init__(self)
		self.ip=ip
		self.timeout = timeout
		self.icmpId = icmpId
	def run(self):
		# if os.name== "nt" or os.uname()[0] == "Linux":
		# if platform.uname()[0] in ['SunOS','Linux','Windows']:
		if platform.system() in ['SunOS','Linux','Windows']:
			pingObject= PYPING(self.ip,self.icmpId,self.timeout)
			self._out = pingObject.ping()
			if self._out['status'] == 'up':
				G_logD['CURREPORT']['PINGLIST'].append({"ip":self.ip,"status":"up","datetime":getDateTime(),'timeMs':self._out['time'],'ttl':self._out['ttl']})
			else:
				G_logD['CURREPORT']['PINGLIST'].append({"ip":self.ip,"status":"down","datetime":getDateTime(),'timeMs':self._out['time'],'ttl':self._out['ttl']})
		else:
			puts('Can not running by this system!')
			sys.exit(1)

#下面为两种不同的多线程执行方式
#多线程组同时调用执行
def threadPing(host=None,_threadNumi=10,_timeoutf=0.1):
	'''threadPing  函数调用说明
		分组方式执行：分多个线程同时跑
		thread_num  #同时运行线程数
		host #需要ping测试主机数
	'''
	global G_logD
	if type(host) == type([]):
		t=None
		_hostsCounti = len(host)  #显示运行进度
		_threadNumi #同时运行线程数
		puts('[%s]start running ping ip : %d  -> now 0%%' % (getDateTime(),_hostsCounti))
		icmpBaseId = os.getpid()
		
		for i in range(len(host)):
			#设置各个主要ping的icmp包识别号
			icmpId = (icmpBaseId + i) & 0xFFFF
			PINGTHREAD(host[i],icmpId,_timeoutf).start()
			#限制执行最大并发ping数
			if threading.active_count() == _threadNumi:
				while True:
					if threading.active_count() < _threadNumi:
						break
					else:
						time.sleep(0.1)
				#进度提示
		#防止进入死循环中, 等待所有任务获取到结果
		while True:
			if threading.active_count() == 0 or len(G_logD['CURREPORT']['PINGLIST']) == _hostsCounti:
				break
			else:
				time.sleep(0.1)
				# _ti += 1
	else:
		puts('Ping start ping Error!',threadPing)

#ping日志输出函数
def putsRport(_report=None):
	'''ping日志输出函数'''
	global G_logD
	if _report != None:
		puts('%s%s%s%s%s'%('ip'.ljust(20),'status'.ljust(10),'time(ms)'.ljust(12),'TTL'.ljust(10),'datetime'))
		_upi = 0
		G_logD['CURREPORT']['COUNT'] = len(_report)
		for _i in _report:
			puts('%s%s%s%s%s'%(_i['ip'].ljust(20),_i['status'].ljust(10),_i['timeMs'].ljust(12),str(_i['ttl']).ljust(10),_i['datetime']))
			if _i['status'] == 'up': _upi += 1
	G_logD['CURREPORT']['UP'] = _upi     # 统计up端口数
	G_logD['CURREPORT']['DOWN'] = G_logD['CURREPORT']['COUNT'] - G_logD['CURREPORT']['UP']  # 统计down端口数
	G_logD['CURREPORT']['ENDTIME'] = getDateTime()
	puts("ip4ping Ip #count:%s up:%s down:%s #start [%s] -  end [%s]"%(G_logD['CURREPORT']['COUNT'],G_logD['CURREPORT']['UP'],G_logD['CURREPORT']['DOWN'],G_logD['CURREPORT']['STARTTIME'],G_logD['CURREPORT']['ENDTIME']))
'''
#==============================================================
#端口扫描
#  日期:2014-01-27 11:00
#TCP端口扫描 因是长连接，需要占用系统端口，因此需要注意链接数，避免影响系统正常使用
#==============================================================
'''
#全局变量配置等，详见 G_etcD

#建立端口扫描多线程组
class PORTTHREAD(threading.Thread):
	'''端口多线程扫描'''
	def __init__(self,_hosts,_ports):
		threading.Thread.__init__(self)
		self._hosts = _hosts
		self._ports = _ports
		socket.setdefaulttimeout(0.1)  #设置socket超时时间
	def run(self):
		try:
			self.__socketO = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
			self.__socketO.connect((self._hosts,self._ports))
			puts("%s:%d open" % (self._hosts,self._ports))
			self.__socketO.close()
		except:
			# puts('scan port %s no open' % self._ports)
			pass
#端口扫描class
class PYPORTS:
	'''端口扫描class'''
	def __init__(self,hosts='127.0.0.1',portsL=None):
		if isinstance(portsL,list):
			self._scanPortsL = portsL
		else:
			self._scanPortsL = [i for i in range(1,1025,1)]
		try:
			socket.gethostbyname(hosts)  #域名解释
		except:
			puts("Hostname %s unknow" % hosts)
			sys.exit(1)
		self._hosts = hosts
		self.__scan()
	def __scan(self):
		for _tport in self._scanPortsL:
			# puts('scan port %s' % _tport)
			PORTTHREAD(self._hosts,_tport).start()
			#限制执行最大并发ping数
			if threading.active_count() == G_etcD['SCANPORT']['MAX_THREADS']:
				while True:
					if threading.active_count() < G_etcD['SCANPORT']['MAX_THREADS']:
						break
					else:
						time.sleep(0.1)
		#等待所有扫描线程结束  等待5秒后，自动结束所有线程
		_timeOuti = 0
		while True:
			if threading.active_count() == 0 or _timeOuti <= 5:
				break
			else:
				time.sleep(0.1)
				_timeOuti += 1


'''
#==============================================================
#交互shell
#日期:2014-01-23 16:04
#
#==============================================================
'''
class PYSH(cmd.Cmd):
	'''ip4ping 交互SHELL'''
	__prompt = "[ip4ping]"
	def __init__(self,_tprompt='#'):
		cmd.Cmd.__init__(self)
		self.prompt = self.__prompt + _tprompt
	def do_include(self, arg):
		'''导入功能模块
		使用格式：include file.py
		'''
		execfile(arg)
	def do_py(self, arg):
		'''执行单行python 代码
	使用格式：py print "dsfs"
		计算网段地址，可使用地址:
			py print ipNetCount('192.168.0.1/28')
		'''
		try:
			exec(arg)
		except:
			puts("代码执行异常")
	def do_ping(self,arg):
		'''ping net 
Usage:
	ping 192.168.1.0/28
		'''
		if re.match(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}[\\/]\d{2}',arg):
			threadPing(ipNetCount(arg),G_etcD['PING']['MAX_THREADS'])
			putsRport(G_logD['CURREPORT']['PINGLIST'])
		else:
			puts('ping use error! Usage: ping 192.168.1.0/28')
	#端口扫描  日期:2014-01-27 11:04
	def do_scan(self,arg):
		'''scan 
Usage: scan [host|net] startport endport
	scan host 192.168.1.1 1 1024
	scan net 192.168.0.1[/\]24 1 1024
		'''
		puts(arg)
		if re.match(r'^[\whost\wnet]',arg):
			_optionL = re.split(r'\s+',arg)
			if _optionL[0] == 'host':
				if re.match(r'host \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} \d{1,4} \d{1,4}',arg):
					_start = int(_optionL[2]) 
					_end = int(_optionL[3]) 
					if _start < _end:
						_postsL = [i for i in range(_start,_end,1)]
					PYPORTS(_optionL[1],_postsL)
				else:
					puts('host command error!')
			elif _optionL[0] == 'net':
				if re.match(r'net \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}[\\/]\d{2} \d{1,4} \d{1,4}',arg):
					_ipListL = ipNetCount(_optionL[1])
					_start = int(_optionL[2]) 
					_end = int(_optionL[3]) 
					if _start < _end:
						_postsL = [i for i in range(_start,_end,1)]
					for _thost in _ipListL:
						puts('scan %s port[%d - %d]' %(_thost,_start,_end))
						PYPORTS(_thost,_postsL)
		else:
			puts('command error!')
	def do_conf(self,arg):
		_cli = PYSH('[Conf]')
		_cli.cmdloop()
	#执行系统命令
	def do_sh(self, arg):
		'''本机系统命令调用
	使用格式：sh cmd
	应用实例：
		sh netstat -rn 
		#执行netstat 命令，获取系统路由表配置信息
		sh tasklist 
		#获取系统执行进程信息
		'''
		try:
			lines = os.popen(arg).readlines()
			for i in lines:
				puts(i.strip())
		except:
			puts('not find the command!')
	#退出
	def do_quit(self, arg):
		sys.exit(1)
	def do_exit(self,arg):
		sys.exit(1)
	# 帮助    
	def help_quit(self):
		puts("syntax:  quit")
		puts("--  terminates  the  application")
	#快捷命令
	do_q = do_quit

# 开始执行代码
if __name__ == "__main__":
	if len(sys.argv) in [2,3]:
		if len(sys.argv)==3:
			_num=int(sys.argv[2])
			if _num <= 150:
				G_etcD['PING']['MAX_THREADS'] = _num
			else:
				G_etcD['PING']['MAX_THREADS'] = 100
		if os.path.isfile(sys.argv[1]):
			# lines=open(sys.argv[1],'r').readlines()
			_hosts = [_i for _i in re.split(r'[\s\t]+',i)[0] for i in open(sys.argv[1],'r').readlines() if i[0]!='#']
			threadPing(_hosts,G_etcD['PING']['MAX_THREADS'])
			del _hosts
			putsRport(G_logD['CURREPORT']['PINGLIST'])
		#直接ping hosts配置
		elif sys.argv[1] == 'hosts':
			if os.name == "nt":
				if os.path.isfile('C:\\WINDOWS\\system32\\drivers\\etc\\hosts'):
					_hosts=[re.split(r'\s+',i)[0] for i in [ i.strip() for i in open("C:\\WINDOWS\\system32\\drivers\\etc\\hosts","r").readlines()] if re.match(r'[^#]',i)]
					threadPing(_hosts,G_etcD['PING']['MAX_THREADS'])
					del _hosts
					putsRport(G_logD['CURREPORT']['PINGLIST'])
				else:
					puts('can not find the system hosts config file!')
			elif os.name == "posix":  #linux系统  solairs x86
				_hosts=[re.split(r'\s+',i)[0] for i in [ i.strip() for i in open("/etc/hosts","r").readlines()] if re.match(r'[^#a-zA-Z\W]',i)]
				threadPing(_hosts,G_etcD['PING']['MAX_THREADS'])
				del _hosts
				putsRport(G_logD['CURREPORT']['PINGLIST'])
			else:
				puts('can not find the system hosts config file!')
		#ping 网段
		elif re.match(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}[\\/]\d{2}',sys.argv[1]):
			threadPing(ipNetCount(sys.argv[1]),G_etcD['PING']['MAX_THREADS'])
			putsRport(G_logD['CURREPORT']['PINGLIST'])
		#调用一个shell
		elif sys.argv[1] == 'shell':
			cli = PYSH()
			cli.cmdloop()
		else:
			print('hosts file not exist!')
	else:
		print(G_etcD['LANG']['Usage'])